package IC.SemanticChecks;

import java.util.HashSet;
import java.util.Iterator;

import IC.Constants;
import IC.AST.*;
import IC.Types.TypeTable;

/**
 * ***	Bonus	****
 * a propagating visitor class that is used to check that every variable is initialized before used, 
 * and that a function with non-void return value returns it on every control path
 */
public class ReturnInitCheckingVisitor implements PropagatingVisitor<HashSet<String>, Boolean> {
	
	public Boolean visit(Program program, HashSet<String> d) throws Exception {
		for (ICClass c : program.getClasses()) 
			c.accept(this, null);
		return null;
	}

	
	public Boolean visit(ICClass icClass, HashSet<String> d) throws Exception {
		for (Method m : icClass.getMethods()) 
			m.accept(this, null);
		return null;
	}

	
	public Boolean visit(VirtualMethod method, HashSet<String> d) throws Exception {
		HashSet<String> vars = new HashSet<String>();
		boolean returnOnEveryPath = false;
		for (Statement stmt : method.getStatements()) 
			returnOnEveryPath =  stmt.accept(this, vars) || returnOnEveryPath;
		
		boolean isVoidReturnType = method.getScope().lookup(Constants.RETURN).getType().equals(TypeTable.voidType);
		if (returnOnEveryPath == false && !isVoidReturnType) 
			SemanticError.handle(SemanticErrorType.NO_RETURN, method.getLine());
		
		return null;
	}

	
	public Boolean visit(StaticMethod method, HashSet<String> d) throws Exception {
		HashSet<String> vars = new HashSet<String>();
		boolean returnOnEveryPath = false;
		for (Statement stmt : method.getStatements()) 
			returnOnEveryPath = stmt.accept(this, vars) || returnOnEveryPath;
		
		boolean isVoidReturnType = method.getScope().lookup(Constants.RETURN).getType().equals(TypeTable.voidType);
		if (returnOnEveryPath == false && !isVoidReturnType) 
			SemanticError.handle(SemanticErrorType.NO_RETURN, method.getLine());
		
		return null;
	}
	
	
	public Boolean visit(Assignment assignment, HashSet<String> d) throws Exception {
		if (assignment.getVariable() instanceof VariableLocation) {
			VariableLocation varLocation = (VariableLocation) assignment.getVariable();
			if (!varLocation.isExternal()) 
				d.remove(varLocation.getName());
		}
		assignment.getVariable().accept(this, d);
		assignment.getAssignment().accept(this, d);
		return false;
	}
	
	
	public Boolean visit(VariableLocation location, HashSet<String> d) throws Exception {
		if (!location.isExternal()) {
			if (d.contains(location.getName())) 
				SemanticError.handle(SemanticErrorType.INVALID_VARIABLE_INIT, location.getLine());
		}
	    else 
			location.getLocation().accept(this, d);
		return false;
	}

	
	public Boolean visit(ArrayLocation location, HashSet<String> d) throws Exception {
		location.getArray().accept(this, d);
		location.getIndex().accept(this, d);
		return false;
	}

	
	public Boolean visit(If ifStatement, HashSet<String> d) throws Exception {
		boolean returnOnEveryPath = false;
		ifStatement.getCondition().accept(this, d);
		if (ifStatement.hasElse()) {
			HashSet<String> notInitVars1 = new HashSet<String>(d);
			returnOnEveryPath = ifStatement.getOperation().accept(this, notInitVars1);
			HashSet<String> notInitVars2 = new HashSet<String>(d);
			returnOnEveryPath = ifStatement.getElseOperation().accept(this, notInitVars2) && returnOnEveryPath;
			
			for (Iterator iterator = d.iterator(); iterator.hasNext();) {
				String oldNotInitVar  = (String) iterator.next();
				if (!notInitVars1.contains(oldNotInitVar) && !notInitVars2.contains(oldNotInitVar)) 
					iterator.remove();
			}
			
		} else {
			HashSet<String> copyOfNotInitVars = new HashSet<String>(d);
			ifStatement.getOperation().accept(this, copyOfNotInitVars);
		}
		return returnOnEveryPath;
	}

	
	public Boolean visit(While whileStatement, HashSet<String> d) throws Exception {
		whileStatement.getCondition().accept(this, d);
		HashSet<String> copyOfNotInitVars = new HashSet<String>(d);
		whileStatement.getOperation().accept(this, copyOfNotInitVars);
		return false;
	}

	
	public Boolean visit(StatementsBlock statementsBlock, HashSet<String> d) throws Exception {
		boolean returnOnEveryPath = false;
		HashSet<String> notInitVars = new HashSet<String>(d);
		for (Statement stmt : statementsBlock.getStatements())
			returnOnEveryPath = stmt.accept(this, notInitVars) || returnOnEveryPath;
		
		for (Iterator iterator = d.iterator(); iterator.hasNext();) {
			String oldNotInitVar  = (String) iterator.next();
			if (!notInitVars.contains(oldNotInitVar) && !notInitVars.contains(oldNotInitVar)) 
				iterator.remove();
		}
		
		return returnOnEveryPath;
	}

	
	public Boolean visit(LocalVariable localVariable, HashSet<String> d) throws Exception {
		if (!localVariable.hasInitValue()) 
			d.add(localVariable.getName());
		else 
			localVariable.getInitValue().accept(this, d);
		return false;
	}

	
	public Boolean visit(VirtualCall call, HashSet<String> d) throws Exception {
		for (Expression expr : call.getArguments()) 		
			expr.accept(this, d);
		
		if (call.isExternal()) 
			call.getLocation().accept(this, d);
		return false;
	}
	
	
	public Boolean visit(StaticCall call, HashSet<String> d) throws Exception {
		for (Expression expr : call.getArguments()) 		
			expr.accept(this, d);
		return false;
	}

	
	public Boolean visit(Length length, HashSet<String> d) throws Exception {
		length.getArray().accept(this,d);
		return false;
	}

	
	public Boolean visit(MathBinaryOp binaryOp, HashSet<String> d) throws Exception {
		binaryOp.getFirstOperand().accept(this, d);
		binaryOp.getSecondOperand().accept(this, d);
		return false;
	}

	
	public Boolean visit(LogicalBinaryOp binaryOp, HashSet<String> d) throws Exception {
		binaryOp.getFirstOperand().accept(this, d);
		binaryOp.getSecondOperand().accept(this, d);
		return false;
	}

	
	public Boolean visit(MathUnaryOp unaryOp, HashSet<String> d) throws Exception {
		unaryOp.getOperand().accept(this, d);
		return false;
	}

	
	public Boolean visit(LogicalUnaryOp unaryOp, HashSet<String> d) throws Exception {
		unaryOp.getOperand().accept(this, d);
		return false;
	}

	
	public Boolean visit(ExpressionBlock expressionBlock, HashSet<String> d) throws Exception {
		expressionBlock.getExpression().accept(this, d);
		return false;
	}
	
	
	public Boolean visit(Return returnStatement, HashSet<String> d) throws Exception {
		if (returnStatement.hasValue())
			returnStatement.getValue().accept(this, d);
		return true;
	}
	
	
	public Boolean visit(NewArray newArray, HashSet<String> d) throws Exception {
		newArray.getSize().accept(this, d);
		return false;
	}
	
	
	public Boolean visit(CallStatement callStatement, HashSet<String> d) throws Exception {
		callStatement.getCall().accept(this, d);
		return false;
	}

	
	public Boolean visit(NewClass newClass, HashSet<String> d) {
		return false;
	}

	
	public Boolean visit(This thisExpression, HashSet<String> d) {
		return false;
	}
	
	
	public Boolean visit(Literal literal, HashSet<String> d) {
		return false;
	}
	
	
	public Boolean visit(UserType type, HashSet<String> d) throws Exception {
		return false;
	}

	
	public Boolean visit(Break breakStatement, HashSet<String> d) {
		return false;
	}

	
	public Boolean visit(Continue continueStatement, HashSet<String> d) {
		return false;
	}

	
	public Boolean visit(PrimitiveType type, HashSet<String> d) throws Exception {
		return false;
	}
	
	
	public Boolean visit(Field field, HashSet<String> d) throws Exception {
		return false;
	}
	
	
	public Boolean visit(LibraryMethod method, HashSet<String> d) throws Exception {
		return false;
	}

	
	public Boolean visit(Formal formal, HashSet<String> d) throws Exception {
		return false;
	}

}